iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 25
0
Modern Web

Angular新手村學習筆記(2019)系列 第 25

Day25_CaseStudy: RxJS真實案例展示

  • 分享至 

  • xImage
  •  

[S05E07] CaseStudy: RxJS真實案例展示
https://www.youtube.com/watch?v=zRFb41TfO5Y&list=PL9LUW6O9WZqgUMHwDsKQf3prtqVvjGZ6S&index=16

庫存文章逼得很緊(2篇),努力看影片做筆記
不知道對初學者有沒有一點幫助,不過我個人真的學超多的
(等第30天再來混一篇心得)

今天由jiaming大大分享自身經驗,
非常的珍貴,建議「影片」配合「jiaming大大的文章」一起看
因為影片中有蠻多的討論跟講解

jiaming大大的文章不只是講結果,還有
1.目標(題目)
2.預期結果
3.拆解問題
4.思維

jiaming大大的文章
https://jiaming0708.github.io/2018/08/20/rx-sample/
AutoComplete完整範例
https://github.com/jiaming0708/RxSample
進階範例
https://github.com/jiaming0708/draw-demo

AutoComplete

首先clone「AutoComplete完整範例」來玩
用RxJS實現WebAPI傳回來的資料重組,重組成我們要的型別

前端用input讓使用者輸入關鍵字
再打用Web API要資料「行政院環境保護署。環境資源資料開放平臺」

真的很感謝環保署對資料科學的貢獻

練習用Web API
公民營廢棄物清除機構資料
https://opendata.epa.gov.tw/Data/Contents/WROrg

資料格式
https://opendata.epa.gov.tw/webapi/api/rest/datastore/355000000I-001154?sort=County&offset=0&limit=1000

[
    {
        "County":"宜蘭縣",
        "OrgType":"清除",
        "Grade":"甲",
        "OrgName":"境庭有限公司",
        "RegistrationNo":"G3004187",
        "OrgAddress":"宜蘭縣宜蘭市文昌路一九八之六號一樓",
        "TreatMethod":"",
        "GrantDeadline":"2022/7/18 上午 12:00:00",
        "OrgTel":"03-9356440",
        "OperatingAddress":"宜蘭縣宜蘭市文昌路一九八之六號一樓"
    },...
]
相對應的data model
export interface Waste {
  County: string;
  OrgType: string;
  Grade: string;
  OrgName: string;
  RegistrationNo: string;
  OrgAddress: string;
  TreatMethod: string;
  GrantDeadline: string;
  OrgTel: string;
  OperatingAddress: string;
}

app.component.html

<form [formGroup]="form">
  <input formControlName="keyword">
   ^^^^^ 輸入keywork
  <ul>                                VVVVV 就不用做unsubscribe
    <li *ngFor="let item of wastes$ | async as wastes">
      {{item.OrgName}} 到環保署的WebAPI要資料,做前端filter,show出來
    </li>
  </ul>
  Kevin建議寫法,避免沒值的情況
  <ul *ngIf="wastes$ | async as wastes">
      <li *ngFor="let item of wastes">
          {{item.OrgName}}
      </li>
  </ul>
</form>

app.component.ts

export class AppComponent implements OnInit {
  keyword$: Observable<string>;
  form: FormGroup;
  wastes$: Observable<Waste[]>;

  // "OrgName":"境庭有限公司" 找OrgName欄位有沒有「keyword」
  // index = -1就是沒有
  hasKeyword = (keyword: string) => {
    return (waste: Waste) => waste.OrgName.indexOf(keyword) > -1;
                                   ^^^^^^^
  }

  ngOnInit() {
    this.form = this.fb.group({
      keyword: ['']
    });
    this.form.get('keyword').valueChanges
                             ^^^^^^^^^^^
                             reactive form 本身 value changes的監聽
      .subscribe(p=>console.log(p));
                 ^ 當keyword改變時,就抓的到
    
    // 這裡的2個observable分別是this.keyword$跟this.envAPI.wasteAPI$
    // switchMap就是:
    // 每個this.keyword$資料流,都會丟到this.envAPI.wasteAPI$做運算
    // mergeMap 我猜是把this.envAPI.wasteAPI$的結果併起來
    
    // way1: `mergeMap`
    this.wastes$ = this.keyword$.pipe(
      debounceTime(200), // 每個字等0.2秒,減少送http request的次數
      switchMap(keyword => this.envAPI.wasteAPI$.pipe(
      ^^^^^^^^ 等於 map + switch
        mergeMap(p => from(p)), // Observable<Waste>
        ^^^^^^^^^  api吐回來的資料流合併,不管順序
        filter(this.hasKeyword(keyword)),
        toArray() // 把filter完的結果,用toArray()轉成陣列
      ))
    );

    // switchMap 會在下一個 observable 被送出後直接退訂前一個未處理完的 observable
    // 如果新資料已回來,會取消較舊的keyword(還沒回來的),只保留最後1次
    // 適合用在發送 HTTP request
    
    // https://rxjs-dev.firebaseapp.com/api/operators/switchMap
    // 看圖比較好懂
    // const switched = of(1, 2, 3).pipe(switchMap((x: number) => of(x, x ** 2, x ** 3)));
                           ^                                         ^  ^^^^^^  ^^^^^^
    // switched.subscribe(x => console.log(x));
    // 輸出: 1 1 1、2 4 8、3 9 27

    // 沒有用RxJS的寫法。若用RxJS改寫,可以把每個步驟拆開來看
    var result = []; // 建一個新陣列
    array.forEach(p => {
          ^^^^^^^ 對應上面的from(p)
        if(p.OrgName.indexOf(keyword) > -1){
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ filter(this.hasKeyword(keyword))
            result.push(p); // 把符合條件的資料塞到陣列中
            ^^^^^^^^^^^^^^ toArray()
        }
    });
    
    優化
    // way2: combineLatest
    // 2個observable:this.keyword$、this.envAPI.wasteAPI$
    this.wastes$ = combineLatest(
      this.keyword$.pipe( 
        debounceTime(200)
      ), this.envAPI.wasteAPI$
    ).pipe( // 拿最後1次的值輸出
      map(([keyword, wastes]) => wastes.filter(this.hasKeyword(keyword)))
    );

資料轉換

練習資料長這樣:

[
    {
      modifyTime: '2018/04/27',
      criticalLevel: 3,  不一定有
      oddsLevel: 3       不一定有
    },
    {
      modifyTime: '2018/03/22',    把不完整的資料乎略
    }
]

範例一:

[
    {
      id: `${oddsLevel}-${criticalLevel}`,
      date: '',  modifyTime欄位改名
      order: 0   多加一個欄位
    }
]
getPointList() {
  return obs => obs.pipe(
    // 步驟1
    switchMap((p: HistoryRecord[]) => p),
    ^^^^^^^^^ 指定型別,接傳回來的資料,也可以用mergeMap(p => from(p))
    // 步驟2
    filter((p: HistoryRecord) => !!p.criticalLevel && !!p.oddsLevel),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            callback function,回傳false就不要
    // 步驟3 用map()組合成想要的東西
    map((p: HistoryRecord) => ({
      id: `${p.oddsLevel}-${p.criticalLevel}`,
      date: p.modifyTime
    } as PointData)),
         ^^^^^^^^^ Object的型別
    toArray(),  // 轉陣列
    map((p: PointData[]) => p.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))),
                              ^^^^  ^^^^ compareFunction(a,b)
// javascript 的 Array.prototype.sort()
// compareFunction(a, b) 若<0,a排在b前面;若=0不變;若>0,b排在a前面
// https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
    map((p: PointData[]) => p.map((data, idx) => ({ ...data, order: idx })))
                                                    原陣列    ^^^^ 加欄位
  );
}

範例二:

[
    {
      id: `${oddsLevel}-${criticalLevel}`,
      total: 0,    多加一個欄位,array資料筆數
      current: 0   多加一個欄位
    }
]
// 常用情境:
// 如果有2個api,可以用mergeMap、reduce處理、map重新組合
getPointCount() {
         vvv obs是observable,可以想像成被訂閱時,外面的資料流進來,再pipe處理
  return obs => obs.pipe(
         ^^^ higher-order function(高階)寫法,obs是一個function
// https://jcouyang.gitbooks.io/functional-javascript/content/zh/!higher-order-function.html
                                         vvvv 轉Observable
    mergeMap((pointList: PointData[]) => from(pointList) 
              ^^^^^^^^^^^^^^^^^^^^^^要保留在後面用,才需要用mergeMap
        .pipe( // 用reduce整理資料
          reduce((acc, value: PointData) => {
          ^^^^^^ RxJS跟javascript都有reduce
            const id = value.id;
            if (!acc[id]) {
              acc[id] = { total: 0, current: 0 } as PointCount;
            }
            acc[id].total++;
            return acc;
          }, [] as PointCount[]),
          // 把2個陣列,組成Object
          map(counts => ({ pointList: pointList, pointCount: counts }))
                           ^^^^^^^^^^^^^^^^^^^^
        ))
  );
}

// return obs => obs.pipe(
//     switchMap((p: HistoryRecord[]) => p),
// 可以寫成
// return (obs: Observable<HistoryRecord[]>) => obs.pipe(
//                        ^^^^^^^^^^^^^^^資料流的型別
//     switchMap(p=>p) 後面的p就知道型別
// 也可以寫成
// return (obs: Observable<HistoryRecord[]>) => obs.pipe<PointData[]>(
                                                        ^^^^^^^^^^^^^
// pipe支援泛型,給的型別其實就是pipe最後出來的資料長什麼樣子
// 好處是靜態分析會多個檢查
                                                        
this.api.dataApi$.pipe(
  filter(p => !!p && p.length > 0),
  this.getPointList(), // 回傳型別 :(obs:any)=>any
  this.getPointCount()
).subscribe( (p:{pointList, pointCount}) => {
                ^^^^^^^^^^  ^^^^^^^^^^^
// 這個邊才告知getPointList()跟getPointCount()回傳的型別(資料長什麼樣子)
// 如果沒正常回傳會報錯嗎? 是否要串catchError()?
    this.api.pointList$.next(p.pointList);
});

範例二預期輸出:
輸出一個Object,這個Object包含2個Array(練習一、練習二)

動態資料監聽

https://jiaming0708.github.io/2018/08/20/rx-sample/#%E5%8B%95%E6%85%8B%E8%B3%87%E6%96%99%E7%9B%A3%E8%81%BD

資料流是從API下來,假設經過component處理後輸出
我們要監聽這些資料流所經過component的資料(經過幾個component就會有幾筆資料)

理後的資料
範例三

// 1
{
  top: 10, // 經過的component的top
  left: 10, // component的left
  order: 0
}
// 2
{
    top: 20,
    left: 20,
    order: 1
}…

最後期望的資料
第一組 第二組 第三組
[[1, 2], [2, 3], [3, 4]]
^ ^

拆解問題

  1. 監聽component送出的資料
  2. 確認要取得的資料筆數
  3. 根據資料做排序
    資料流經過component後,接收到資料的順序不一定,所以用order排序
  4. 組成兩兩一組的資料(pairewise) // [1, 2]
    包含Observable除了next(),最後要給complete(),
    才會把整個資料(最後期望的資料)吐出來

怎麼知道WebAPI的資料已取完?

this.api.pointList$
  .pipe(
    mergeMap(pointList =>
      this.api.elementPoint$
        .pipe(
          bufferCount(pointList.length),
          tap(p=>console.log(p)), // bufferCount吐出來的是array
          switchMap(p => p.sort((a, b) => a.order - b.order)),
          pairwise(),
          bufferCount(pointList.length - 1, 1),  // 這行看不懂
                      起始位置               取幾筆
          // 如果沒有bufferCount,會全取所有資料
        )))
  .subscribe(p => console.log(p));

上一篇
Day24_RxJS 運算子全面解析(2/2)
下一篇
Day26_RxJS Custom Operators(1/3)
系列文
Angular新手村學習筆記(2019)33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言